package manip_exec

import (
	"fmt"
	"go/ast"
	"go/format"
	"go/parser"
	"go/token"
	"math/rand"
	"os"
	"os/exec"
	"path/filepath"
	"resbuilder"
	"strconv"
	"strings"
	"time"
)

//Specifies if the manipulation should be random or sequential
const (
	RAND = iota
	SEQ
)

//Specifies the position in the args array
const (
	INPUT_DIR = iota
	INPUT_FILE
	RANDSEQ
	OUTPUT_DIR
	OUTPUT_FILE
	PACKAGE_NAME
	TEST_FILE
	TEST_OUTPUT
	RESULTOUT_DIR
	ORIGINAL
	CHANGED
	BUILDFAILED
)

var cmd_args = make([]string, 12)

type ManiRes struct {
	Poss    []int
	Changed string
}

type Manipulator interface {
	ManipulateAST(ast.Node, int) (ManiRes, error)
	String() string
}

type Manip_Exec struct {
	Manips  []Manipulator
	Results []int
}

func (this *Manip_Exec) AddManipulator(m Manipulator) {
	(*this).Manips = append((*this).Manips, m)
}

func nameFilter(filename string) bool {

	if strings.Contains(filename, ".go") && !strings.Contains(filename, "_test") {
		return true
	}
	return false
}

func dirFilter(f os.FileInfo) bool { return nameFilter(f.Name()) }

func (this *Manip_Exec) ManipulateAll(args []string, iters int) {

	cleanDir(args)
	fset := token.NewFileSet()

	results := make(map[string]int)

	//Loop over Manipulators
	for _, v := range (*this).Manips {

		//Iterations loop
		for j := 0; j < iters; j++ {

			dirMap, err := parser.ParseDir(fset, os.Getenv("GOPATH")+"/src/"+args[0], dirFilter, 0)
			if err != nil {
				panic("Unable to parse Directory")
			}

			//Loop over files
			for key, file := range dirMap[args[0]].Files {

				currentfilename := strings.TrimRight(filepath.Base(key), ".go")

				cmd_args = make([]string, 12)
				randseq, args := catchArgs(args)
				useArgs(args[0], currentfilename, v.String())
				setupDir(cmd_args, v.String())

				random := randomize(randseq, j)

				f := file

				changed, err := v.ManipulateAST(f, random)
				if err != nil {
					panic(err)
				}

				cmd_args[CHANGED] = changed.Changed + lineGet(changed.Poss, fset) //Append the change string
				falsepositive := buildntest(cmd_args, fset, f)
				if falsepositive {
					results[key+v.String()] = results[key+v.String()] + 1 //Update the result map
				}
				copyOutFile(cmd_args, j)
			}
		}

		//Parse dir to get the file list again	: inefficient...
		dirMap, err := parser.ParseDir(fset, os.Getenv("GOPATH")+"/src/"+args[0], dirFilter, 0)
		if err != nil {
			panic("Unable to parse Directory")
		}

		//Loop over files to print cmd args
		for key, _ := range dirMap[args[0]].Files {
			f0, err := os.OpenFile(filepath.Dir(key)+"/ManipulationTests/"+strings.TrimRight(filepath.Base(key), ".go")+"_"+v.String()+"_mani"+"/diffresults/TestOutput.txt", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
			if err != nil {
				panic("Results output error")
			} else {
			f0.WriteString(fmt.Sprintf("Cmd Args: %s %s %d\n", cmd_args[ORIGINAL], args[1], iters))
			f0.WriteString(fmt.Sprintf("%d", results[key+v.String()]))
			}
		}
	}
}

func randomize(randseq, index int) int {

	random := 0
	if randseq == RAND {
		rand.Seed(int64(time.Now().Nanosecond())) //Seed the random generator
		random = rand.Int()
	} else {
		random = index
	}
	return random
}

func catchArgs(args []string) (int, []string) {

	var randseq int64
	var err error

	if len(args) < 1 {
		panic("Need more parameters")
	}

	if len(args) < 2 { //If RandSeq is not specified, assume seq
		randseq = 1
		args = append(args, "1")
	} else {
		randseq, err = strconv.ParseInt(args[1], 0, 0)
		if err != nil {
			panic("Conv failed")
		}
	}

	return int(randseq), args //Needs to return the args, as it is changed sometimes
}

func useArgs(packagename, filename, mani_name string) {

	//Assign the right filenames to each variable
	cmd_args[INPUT_FILE] = os.Getenv("GOPATH") + "/src/" + packagename + "/" + filename + ".go" // INPUT_FILE
	cmd_args[INPUT_DIR] = os.Getenv("GOPATH") + "/src/" + packagename + "/ManipulationTests"

	cmd_args[OUTPUT_DIR] = cmd_args[INPUT_DIR] + "/" + filename + "_" + mani_name + "_mani"      // OUTPUT_DIR
	cmd_args[OUTPUT_FILE] = cmd_args[OUTPUT_DIR] + "/" + filename + "_" + mani_name + "_mani.go" // OUTPUT_FILE

	cmd_args[PACKAGE_NAME] = packagename + "/ManipulationTests/" + filename + "_" + mani_name + "_mani" // PACKAGE_NAME

	cmd_args[TEST_FILE] = os.Getenv("GOPATH") + "/src/" + packagename + "/" + packagename + "_test.go" //TEST_FILE
	cmd_args[TEST_OUTPUT] = cmd_args[OUTPUT_DIR] + "/" + filename + "_" + mani_name + "_mani_test.go"  // TEST_OUTPUT

	cmd_args[RESULTOUT_DIR] = cmd_args[INPUT_DIR] + "/" + filename + "_" + mani_name + "_mani" + "/diffresults" // RESULTOUT_DIR

	cmd_args[ORIGINAL] = packagename //ORIGINAL
}

func cleanDir(args []string) {

	if err0 := os.Chdir(os.Getenv("GOPATH") + "/src/" + args[0] + "/ManipulationTests"); err0 == nil {
		if err1 := os.RemoveAll(os.Getenv("GOPATH") + "/src/" + args[0] + "/ManipulationTests"); err1 != nil {
			panic("Cannot remove directory to clean")
		}
	}

	err1 := os.Mkdir(os.Getenv("GOPATH")+"/src/"+args[0]+"/ManipulationTests", 0777)
	if err1 != nil {
		panic("Clean: Mkdir failed")
	}

}

func setupDir(args []string, name string) {

	if err1 := os.Chdir(args[RESULTOUT_DIR]); err1 != nil {
		dir, _ := filepath.Split(args[RESULTOUT_DIR])
		err1 := os.Mkdir(dir, 0777)
		if err1 != nil {
			panic("Mkdir err1")
		}
		err2 := os.Mkdir(args[RESULTOUT_DIR], 0777)
		if err2 != nil {
			panic("Mkdir error2")
		}
	}
}

func buildntest(args []string, fset *token.FileSet, f interface{}) bool {

	writeFile(args, fset, f)
	runFile(args)

	//Test
	TestResults := runTests(args)
	success := testOutput(args, TestResults)
	return success

}

func writeFile(args []string, fset *token.FileSet, f interface{}) {

	//If not found, create it
	if err1 := os.Chdir(args[OUTPUT_DIR]); err1 != nil {
		err1 := os.Mkdir(args[OUTPUT_DIR], 0777)
		if err1 != nil {
			panic("Mkdir error")
		}
	}

	//Open file for output
	file1, err := os.Create(args[OUTPUT_FILE])
	if err != nil {
		panic("Could not create output file")
	}

	//Write and convert the AST to source code
	err = format.Node(file1, fset, f)
	if err != nil {
		panic("File write failed")
	}

}

func runFile(args []string) []byte {
	cmd := exec.Command("go", "install", args[PACKAGE_NAME])
	output, err2 := cmd.Output()

	if err2 != nil {
		cmd_args[BUILDFAILED] = "BUILD FAIL"
	} else {
		cmd_args[BUILDFAILED] = ""
	}

	cmd = exec.Command("go", "build", args[OUTPUT_FILE])
	output, err2 = cmd.Output()

	if err2 != nil {
		cmd_args[BUILDFAILED] = "BUILD FAIL"
	} else {
		cmd_args[BUILDFAILED] = ""
	}

	fmt.Sprintf("%s %s\n", output, cmd_args[BUILDFAILED])

	filePipe(args)

	return output
}

func filePipe(args []string) {

	//Open test file original
	file1, err0 := os.Open(args[TEST_FILE])
	if err0 != nil {
		cmd_args[CHANGED] = "No tests found"
	}

	//Create test file for writing to
	file2, err1 := os.Create(args[TEST_OUTPUT])
	if err1 != nil {
		panic("Pipe write error")
	}

	bytebuffer := make([]byte, 0)
	if err0 == nil {
		fileinStat, _ := file1.Stat()
		bytebuffer = make([]byte, fileinStat.Size())
		file1.Read(bytebuffer)
	}

	//Copy test file
	file2.Write(bytebuffer)
}

func lineGet(poss []int, fset *token.FileSet) string {

	tempstring := ""

	for i, _ := range poss {
		tempstring += fmt.Sprintf("%d, ", (fset.Position(token.Pos(poss[i])).Line))
	}
	return tempstring
}

func copyOutFile(args []string, index int) {

	//If not found, create it
	if err1 := os.Chdir(args[RESULTOUT_DIR]); err1 != nil {
		err1 := os.Mkdir(args[RESULTOUT_DIR], 0777)
		if err1 != nil {
			panic("Mkdir error")
		}
	}

	file0, err0 := os.Open(args[INPUT_FILE])  //This is the input file where it is created
	file1, err1 := os.Open(args[OUTPUT_FILE]) //This is the output file where it is created

	file2, err2 := os.Create(args[RESULTOUT_DIR] + "/" + args[ORIGINAL] + ".go")
	file3, err3 := os.Create(args[RESULTOUT_DIR] + "/" + args[ORIGINAL] + "_mani" + strconv.FormatInt(int64(index), 10) + ".go")

	if err0 != nil || err1 != nil {
		panic("Pipe read error")
	}
	if err2 != nil || err3 != nil {
		panic("Pipe write error")
	}

	file0inStat, err2 := file0.Stat()
	file1inStat, err2 := file1.Stat()

	if err2 != nil {
		panic("Path read error")
	}
	//Copy test file
	bytebuffer0 := make([]byte, file0inStat.Size())
	bytebuffer1 := make([]byte, file1inStat.Size())

	file0.Read(bytebuffer0)
	file2.Write(bytebuffer0)

	file1.Read(bytebuffer1)
	file3.Write(bytebuffer1)

	exec.Command("gofmt", "-w", "-comments=false", args[RESULTOUT_DIR]+"/"+args[ORIGINAL]+".go")

}

func testOutput(args []string, TestResults []resbuilder.TestRawCapture) bool {

	passed := true

	var f0 *os.File
	var err error

	f0, err = os.OpenFile(args[RESULTOUT_DIR]+"/TestOutput.txt", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
	if err != nil {
		panic("Test Output Open Failed")
	}

	defer f0.Close()

	f0.WriteString(args[CHANGED])
	fmt.Print(args[CHANGED])

	for _, v := range TestResults {
		passed = passed && v.Passed
	}
	tempstring := ""

	if cmd_args[CHANGED] == "No tests found" {
		tempstring = " INSUFFICIENT TESTS"
		passed = true
	} else if cmd_args[CHANGED] == "Nothing manipulated" {
		tempstring = " BUILD FAIL"
		passed = false
	} else if cmd_args[BUILDFAILED] == "BUILD FAIL" {
		tempstring = " BUILD FAIL"
		passed = false
	} else if passed {
		tempstring = " INSUFFICIENT TESTS"
	} else {
		tempstring = " SUCCESS"
	}

	f0.WriteString(tempstring + "\n")
	fmt.Print(tempstring + "\n")

	if err1 := f0.Close(); err1 != nil {
		panic("File will not close")
	}
	return passed
}

func runTests(args []string) []resbuilder.TestRawCapture {
	cmd := exec.Command("go", "test", "-v", args[PACKAGE_NAME])
	output, err2 := cmd.Output()
	if err2 != nil {
		//log.Fatal(err2)
	}

	built := resbuilder.ResultBuilder(output)
	return built
}
